/*
 * AuthService.java
 *
 * Created on July 1, 2007, 1:22 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.atomojo.app.auth;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.UUID;
import java.util.logging.Logger;
import org.atomojo.app.client.XMLRepresentationParser;
import org.infoset.xml.Document;
import org.infoset.xml.Element;
import org.infoset.xml.Name;
import org.infoset.xml.XMLException;
import org.restlet.Client;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.ChallengeResponse;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Protocol;
import org.restlet.representation.StringRepresentation;

/**
 *
 * @author alex
 */
public class AuthProtocolService implements AuthService
{
   static UUID ROOT_ROLE = UUID.fromString("1434b5c6-5e52-4096-9312-f1060b1a38b5");
   static UUID REALM_ROOT_ROLE = UUID.fromString("497d7f39-f9d2-468b-91c8-34ceca927f78");
   static URI NAMESPACE = URI.create("http://www.atomojo.org/Vocabulary/Auth/2007/1/0");
   static Name SESSION = Name.create(NAMESPACE,"session");
   static Name USER = Name.create(NAMESPACE,"user");
   static Name NAME = Name.create(NAMESPACE,"name");
   static Name EMAIL = Name.create(NAMESPACE,"email");
   static Name GROUP = Name.create(NAMESPACE,"group");
   static Name ROLE = Name.create(NAMESPACE,"role");
   static Logger LOG = Logger.getLogger(AuthProtocolService.class.getName());
   URI serviceBase;
   String authURL;
   String sessionURL;
   Client client;
   Cache<String,User> userCache;
   Cache<String,User> sessionCache;
   Map<String,String> passwords;
   XMLRepresentationParser parser;
   public AuthProtocolService() {
   }
   public URI getServiceURI() {
      return serviceBase;
   }
   public void init(Properties props)
      throws AuthException
   {
      String sbaseURI = props.getProperty("base-uri");
      URI baseURI = sbaseURI==null ? null : URI.create(sbaseURI);
      String href = props.getProperty("href");
      if (href==null) {
         throw new AuthException("Missing 'href' property.");
      }
      serviceBase = baseURI==null ? URI.create(href) : baseURI.resolve(href);
      authURL = serviceBase.resolve("./auth?session=false").toString();
      sessionURL = serviceBase.resolve("./auth/").toString();
      Protocol protocol = Protocol.valueOf(serviceBase.getScheme());
      client = new Client(new Context(LOG),protocol);
      client.getContext().getAttributes().put("hostnameVerifier", org.apache.commons.ssl.HostnameVerifier.DEFAULT);
      parser = new XMLRepresentationParser();
      passwords = new TreeMap<String,String>();
      userCache = new Cache<String,User>(100,2*60*1000) {
         public User fetch(AuthCredentials cred,String alias)
            throws AuthException
         {
            if (cred==null) {
               return null;
            }
            return fetchUser(cred,alias);
         }
         
         public User remove(String alias) {
            passwords.remove(alias);
            return super.remove(alias);
         }

         public String getFacet(User instance) {
            return instance.getAlias();
         }
         
      };
      sessionCache = new Cache<String,User>(100,2*60*1000) {
         public User fetch(AuthCredentials cred,String alias)
            throws AuthException
         {
            if (cred==null) {
               return null;
            }
            return fetchUser(cred,alias);
         }
         
         public User remove(String alias) {
            passwords.remove(alias);
            return super.remove(alias);
         }

         public String getFacet(User instance) {
            return instance.getAlias();
         }
         
      };
   }
   
   public boolean createGroup(AuthCredentials cred,String name)
      throws AuthException
   {
      String groupsURL = serviceBase.resolve("./groups").toString();
      Request request = new Request(Method.POST,groupsURL);
      request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,cred.getName(),cred.getPassword()));
      request.setEntity(new StringRepresentation("<group xmlns='"+NAMESPACE+"' alias='"+name+"'/>",MediaType.APPLICATION_XML));
      Response response = client.handle(request);
      return response.getStatus().isSuccess();
   }
   
   public boolean deleteGroup(AuthCredentials cred,String name)
      throws AuthException
   {
      String groupURL = serviceBase.resolve("./groups/a/"+name).toString();
      Request request = new Request(Method.DELETE,groupURL);
      request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,cred.getName(),cred.getPassword()));
      Response response = client.handle(request);
      return response.getStatus().isSuccess();
   }
   
   public boolean addUserToGroup(AuthCredentials cred,String alias,String name)
      throws AuthException
   {
      String groupURL = serviceBase.resolve("./groups/a/"+name+"/users").toString();
      Request request = new Request(Method.POST,groupURL);
      request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,cred.getName(),cred.getPassword()));
      request.setEntity(new StringRepresentation("<user xmlns='"+NAMESPACE+"' alias='"+alias+"'/>",MediaType.APPLICATION_XML));
      Response response = client.handle(request);
      boolean success = response.getStatus().isSuccess();
      if (!success && response.getStatus().getCode()!=404) {
         throw new AuthException("Failed to add "+alias+" to group "+name+", status="+response.getStatus().getCode());
      }
      return success;
   }
   public boolean removeUserFromGroup(AuthCredentials cred,String alias,String name)
      throws AuthException
   {
      String groupURL = serviceBase.resolve("./groups/a/"+name+"/users").toString();
      Request request = new Request(Method.DELETE,groupURL);
      request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,cred.getName(),cred.getPassword()));
      Response response = client.handle(request);
      boolean success = response.getStatus().isSuccess();
      if (!success && response.getStatus().getCode()!=404) {
         throw new AuthException("Failed to remove "+alias+" from group "+name+", status="+response.getStatus().getCode());
      }
      return success;
   }
   
   public User createUser(AuthCredentials cred,String alias,String name,String email,String password) 
      throws AuthException
   {
      String usersURL = serviceBase.resolve("./users").toString();
      Request request = new Request(Method.POST,usersURL);
      request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,cred.getName(),cred.getPassword()));
      String xml = "<user xmlns='"+NAMESPACE+"' alias='"+alias+"' password='"+password+"'>" +
              (name==null ? "" : "<name>"+name+"</name>") +
              (email==null ? "" : "<email>"+name+"</email>") +
              "</user>";
      request.setEntity(new StringRepresentation(xml,MediaType.APPLICATION_XML));
      Response response = client.handle(request);
      if (!response.getStatus().isSuccess()) {
         throw new AuthException("Failed to create user "+alias+", status="+response.getStatus().getCode());
      }
      try {
         Document userDoc = parser.load(response.getEntity());
         User user = createUser(cred,userDoc);
         userCache.put(alias,user);
         passwords.put(alias,password);
         return user;
      } catch (IOException ex) {
         throw new AuthException("I/O exception while communicating with auth service.",ex);
      } catch (XMLException ex) {
         throw new AuthException("XML Error while parsing result form auth service.",ex);
      }
   }
   
   public boolean deleteUser(AuthCredentials cred,String alias)
      throws AuthException
   {
      throw new AuthException("Not implemented.");
   }
   
   User createUser(AuthCredentials cred,Document userDoc)
      throws AuthException,IOException,XMLException
   {
      Element top = userDoc.getDocumentElement();
      if (top.getName().equals(USER)) {

         String userAlias = top.getAttributeValue("alias");
         String groupsURL = serviceBase.resolve("./users/a/"+userAlias+"/groups").toString();
         // get groups
         Request request = new Request(Method.GET,groupsURL);
         request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,cred.getName(),cred.getPassword()));
         Response response = client.handle(request);
         if (response.getStatus().isSuccess()) {
            Document groupsDoc = parser.load(response.getEntity());
            UUID uuid = UUID.fromString(top.getAttributeValue("id"));
            Element nameE = top.getFirstElementNamed(NAME);
            String name = nameE==null ? null : nameE.getText();
            Element emailE = top.getFirstElementNamed(EMAIL);
            String email = emailE==null ? null : emailE.getText();
            List<String> groups = new ArrayList<String>();
            Iterator<Element> groupElements = groupsDoc.getDocumentElement().getElementsByName(GROUP);
            while (groupElements.hasNext()) {
               groups.add(groupElements.next().getAttributeValue("alias"));
            }
            return new User(userAlias,uuid,name,email,groups);
         } else {
            try {
               throw new AuthException("Cannot communicate with auth service, status="+response.getStatus().getCode()+", message="+response.getEntity().getText());
            } catch (IOException ex) {
               throw new AuthException("Cannot communicate with auth service, status="+response.getStatus().getCode());
            }
         }
      } else {
         throw new AuthException("Unexpected element "+top.getName()+" from auth service.");
      }
      
   }
   
   User fetchUser(AuthCredentials cred,String alias)
      throws AuthException
   {
      String userURL = serviceBase.resolve("./users/a/"+alias).toString();
      Request request = new Request(Method.GET,userURL);
      request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,cred.getName(),cred.getPassword()));
      Response response = client.handle(request);
      if (response.getStatus().isSuccess()) {
         try {
            Document userDoc = parser.load(response.getEntity());
            User user = createUser(cred,userDoc);
            return user;
         } catch (IOException ex) {
            throw new AuthException("I/O exception while communicating with auth service.",ex);
         } catch (XMLException ex) {
            throw new AuthException("XML Error while parsing result form auth service.",ex);
         }
      } else if (response.getStatus().getCode()!=404) {
         try {
            throw new AuthException("Cannot communicate with auth service, status="+response.getStatus().getCode()+", message="+response.getEntity().getText());
         } catch (IOException ex) {
            throw new AuthException("Cannot communicate with auth service, status="+response.getStatus().getCode());
         }
      }
      return null;
   }
   
   public User getUser(AuthCredentials cred,String alias)
      throws AuthException
   {
      return userCache.get(cred,alias);
   }
   
   public Iterator<User> getUsers(final AuthCredentials cred)
      throws AuthException
   {
      String userURL = serviceBase.resolve("./users/").toString();
      Request request = new Request(Method.GET,userURL);
      request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,cred.getName(),cred.getPassword()));
      Response response = client.handle(request);
      if (response.getStatus().isSuccess()) {
         try {
            // TODO: this should stream...
            Document usersDoc = parser.load(response.getEntity());
            final Iterator<Element> users = usersDoc.getDocumentElement().getElementsByName(USER);
            return new Iterator<User>() {
               public boolean hasNext() {
                  return users.hasNext();
               }
               public void remove() {
                  throw new UnsupportedOperationException("remove() is not supported.");
               }
               public User next() {
                  Element userE = users.next();
                  String alias = userE.getAttributeValue("alias");
                  try {
                     User user = userCache.get(null,alias);
                     if (user==null) {
                        // get groups
                        String groupsURL = serviceBase.resolve("./users/a/"+alias+"/groups").toString();
                        Request request = new Request(Method.GET,groupsURL);
                        request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,cred.getName(),cred.getPassword()));
                        Response response = client.handle(request);
                        if (response.getStatus().isSuccess()) {
                           try {
                              Document groupsDoc = parser.load(response.getEntity());
                              UUID uuid = UUID.fromString(userE.getAttributeValue("id"));
                              Element nameE = userE.getFirstElementNamed(NAME);
                              String name = nameE==null ? null : nameE.getText();
                              Element emailE = userE.getFirstElementNamed(EMAIL);
                              String email = emailE==null ? null : emailE.getText();
                              List<String> groups = new ArrayList<String>();
                              Iterator<Element> groupElements = groupsDoc.getDocumentElement().getElementsByName(GROUP);
                              while (groupElements.hasNext()) {
                                 groups.add(groupElements.next().getAttributeValue("alias"));
                              }
                              // we won't cache this
                              return new User(alias,uuid,name,email,groups);
                           } catch (IOException ex) {
                              throw new AuthException("I/O exception while communicating with auth service.",ex);
                           } catch (XMLException ex) {
                              throw new AuthException("XML Error while parsing result form auth service.",ex);
                           }
                        } else {
                           try {
                              throw new RuntimeException("Cannot communicate with auth service, status="+response.getStatus().getCode()+", message="+response.getEntity().getText());
                           } catch (IOException ex) {
                              throw new RuntimeException("Cannot communicate with auth service, status="+response.getStatus().getCode());
                           }
                        }

                     }
                     return user;
                  } catch (AuthException ex) {
                     throw new RuntimeException("Cannot get user "+alias,ex);
                  }
               }
            };
         } catch (IOException ex) {
            throw new AuthException("I/O exception while communicating with auth service.",ex);
         } catch (XMLException ex) {
            throw new AuthException("XML Error while parsing result form auth service.",ex);
         }
      } else  {
         try {
            throw new AuthException("Cannot communicate with auth service, status="+response.getStatus().getCode()+", message="+response.getEntity().getText());
         } catch (IOException ex) {
            throw new AuthException("Cannot communicate with auth service, status="+response.getStatus().getCode());
         }
      }
            
   }
   
   
   public boolean updateUser(AuthCredentials cred,String alias,String name,String email)
      throws AuthException
   {
      String userURL = serviceBase.resolve("./users/a/"+alias).toString();
      Request request = new Request(Method.PUT,userURL);
      request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,cred.getName(),cred.getPassword()));
      String xml = "<user xmlns='"+NAMESPACE+"' alias='"+alias+"'>" +
              (name==null ? "" : "<name>"+name+"</name>") +
              (email==null ? "" : "<email>"+name+"</email>") +
              "</user>";
      request.setEntity(new StringRepresentation(xml,MediaType.APPLICATION_XML));
      Response response = client.handle(request);
      return response.getStatus().isSuccess();
   }
   
   public boolean setPassword(AuthCredentials cred,String alias,String password)
      throws AuthException
   {
      String userURL = serviceBase.resolve("./users/a/"+alias).toString();
      Request request = new Request(Method.POST,userURL);
      request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,cred.getName(),cred.getPassword()));
      String xml = "<password xmlns='"+NAMESPACE+"'>" + password + "</password>";
      request.setEntity(new StringRepresentation(xml,MediaType.APPLICATION_XML));
      Response response = client.handle(request);
      boolean success = response.getStatus().isSuccess();
      if (!success && response.getStatus().getCode()!=404) {
         try {
            throw new AuthException("Cannot communicate with auth service, status="+response.getStatus().getCode()+", message="+response.getEntity().getText());
         } catch (IOException ex) {
            throw new AuthException("Cannot communicate with auth service, status="+response.getStatus().getCode());
         }
      }
      return success;
   }
   
   public User authenticate(String alias,String password)
      throws AuthException
   {
      User user = null;
      String checkPassword = passwords.get(alias);
      if (checkPassword!=null && checkPassword.equals(password)) {
         user = userCache.get(null,alias);
         if (user!=null) {
            return user;
         }
      }
      Request request = new Request(Method.GET,authURL);
      request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,alias,password));
      Response response = client.handle(request);
      if (response.getStatus().isSuccess()) {
         try {
            Document doc = parser.load(response.getEntity());
            user = parseUser(doc);
            User cachedUser = userCache.get(null, alias);
            if (cachedUser!=null) {
               user = cachedUser;
               passwords.put(user.getAlias(),password);
            } else {
               LOG.fine("Caching user "+user.getAlias());
               passwords.put(user.getAlias(),password);
               userCache.put(user.getAlias(),user);
            }
         } catch (IOException ex) {
            throw new AuthException("I/O exception while communicating with auth service.",ex);
         } catch (XMLException ex) {
            throw new AuthException("XML Error while parsing result form auth service.",ex);
         }
      } else if (response.getStatus().getCode()!=401) {
         try {
            if (response.getEntity()!=null) {
               throw new AuthException("Cannot communicate with auth service, status="+response.getStatus().getCode()+", message="+response.getEntity().getText());
            } else {
               throw new AuthException("Cannot communicate with auth service, status="+response.getStatus().getCode());
            }
         } catch (IOException ex) {
            throw new AuthException("Cannot communicate with auth service, status="+response.getStatus().getCode());
         }
      }
      return user;
   }
   
   protected User parseUser(Document doc)
      throws AuthException
   {
      Element top = doc.getDocumentElement();
      if (top.getName().equals(SESSION)) {
         UUID uuid = UUID.fromString(top.getAttributeValue("user-id"));
         String userAlias = top.getAttributeValue("user-alias");
         Element nameE = top.getFirstElementNamed(NAME);
         String name = nameE==null ? null : nameE.getText();
         Element emailE = top.getFirstElementNamed(EMAIL);
         String email = emailE==null ? null : emailE.getText();
         // TODO: group is outdated.  It needs to be replaced
         List<String> groups = new ArrayList<String>();
         Iterator<Element> groupElements = top.getElementsByName(GROUP);
         while (groupElements.hasNext()) {
            groups.add(groupElements.next().getAttributeValue("alias"));
         }
         boolean makeAdmin = false;
         Iterator<Element> roleElements = top.getElementsByName(ROLE);
         while (roleElements.hasNext()) {
            UUID role = UUID.fromString(roleElements.next().getAttributeValue("id"));
            if (role.equals(ROOT_ROLE) || role.equals(REALM_ROOT_ROLE)) {
               makeAdmin = true;
            }
         }
         if (makeAdmin) {
            groups.add(AuthService.ADMIN_GROUP);
         }
         return new User(userAlias,uuid,name,email,groups);
      } else {
         throw new AuthException("Unexpected element "+top.getName()+" from auth service.");
      }
      
   }
   
   public User verifySession(String session)
      throws AuthException
   {
      Request request = new Request(Method.GET,sessionURL+session);
      Response response = client.handle(request);
      if (response.getStatus().isSuccess()) {
         try {
            Document doc = parser.load(response.getEntity());
            User user = parseUser(doc);
            LOG.fine("Caching user "+user.getAlias());
            sessionCache.put(session,user);
            userCache.put(user.getAlias(),user);
            return user;
         } catch (IOException ex) {
            throw new AuthException("I/O exception while communicating with auth service.",ex);
         } catch (XMLException ex) {
            throw new AuthException("XML Error while parsing result form auth service.",ex);
         }
      }
      return null;
   }

}
